package org.mx.utils;

import static java.nio.file.FileVisitResult.CONTINUE;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.mx.utils.SystemUtils.SystemType;

import lombok.extern.slf4j.Slf4j;

/**
 * 文件处理工具类
 *
 * @author : john.peng date : 2017/10/15
 */
@Slf4j
public class FileUtils {
	private FileUtils() {
		super();
	}

	/**
	 * 根据操作系统矫正文件路径，主要处理斜杠和反斜杠。
	 * 
	 * @param path 文件路径
	 * @return 矫正后的路径
	 */
	public static String adjustPath(String path) {
		String adjust;
		if (SystemUtils.getType() == SystemType.Windows) {
			adjust = path.replace('/', '\\');
		} else {
			adjust = path.replace('\\', '/');
		}
		if (log.isDebugEnabled()) {
			log.debug(String.format("Adjust the path: %s -> %s.", path, adjust));
		}
		return adjust;
	}

	/**
	 * 获取文件后缀名
	 * 
	 * @param name 文件名
	 * @return 文件后缀名
	 */
	public static String getType(String name) {
		return name.substring(name.lastIndexOf(".") + 1);
	}

	/**
	 * 按需读取文件内容
	 * 
	 * @param <R>		读取返回类型
	 * @param filePath	文件名
	 * @param supplier	指定的按需处理函数
	 * @return	按需读取的文件内容
	 */
	private static <R> R readFile(String filePath, Function<Path, R> supplier) {
		Path path = Paths.get(filePath);
		File file = path.toFile();
		if (!file.exists()) {
			if (log.isErrorEnabled()) {
				log.error(String.format("The file[%s] not found.", filePath));
			}
			return null;
		}
		if (!file.isFile()) {
			if (log.isErrorEnabled()) {
				log.error(String.format("The file[%s] is not a file.", filePath));
			}
			return null;
		}
		final float size = file.length() / 1024 / 1024;
		if (size >= 3) {
			if (log.isWarnEnabled()) {
				log.warn(String.format("The file[%s]'s size is %.2fM, it would lead to performance issues。", filePath,
						size));
			}
		}
		return supplier.apply(path);
	}

	/**
	 * 读取指定文本文件的内容为一个字符串
	 * 
	 * @param filePath 文件名
	 * @return 字符串
	 */
	public static String readFile2String(String filePath) {
		Function<Path, String> func = (path) -> {
			try {
				return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
			} catch (IOException ex) {
				if (log.isErrorEnabled()) {
					log.error(String.format("Read the file[%s]'s content fail.", filePath), ex);
				}
				return "";
			}
		};
		return readFile(filePath, func);
	}

	/**
	 * 读取指定文本文件的内容，按行分隔的列表
	 * 
	 * @param filePath 文件名
	 * @return 列表
	 */
	public static List<String> readFile2List(String filePath) {
		Function<Path, List<String>> func = (path) -> {
			try {
				return Files.readAllLines(path);
			} catch (IOException ex) {
				if (log.isErrorEnabled()) {
					log.error(String.format("Read the file[%s]'s content fail.", filePath), ex);
				}
				return new ArrayList<>();
			}
		};
		return readFile(filePath, func);
	}

	/**
	 * 读取指定文本文件的内容为字节数组
	 * 
	 * @param filePath 文件名
	 * @return 字节数组
	 */
	public static byte[] readFile2Bytes(String filePath) {
		Function<Path, byte[]> func = (path) -> {
			try {
				return Files.readAllBytes(path);
			} catch (IOException ex) {
				if (log.isErrorEnabled()) {
					log.error(String.format("Read the file[%s]'s content fail.", filePath), ex);
				}
				return new byte[0];
			}
		};
		return readFile(filePath, func);
	}

	/**
	 * 复制指定的文件或目录
	 *
	 * @param source 源
	 * @param target 目标
	 * @throws IOException 复制过程中发生的异常
	 */
	public static void copyFile(String source, String target) throws IOException {
		if (StringUtils.isBlank(source) || StringUtils.isBlank(target) || source.equals(target)) {
			throw new IllegalArgumentException(String.format("source: %s, target: %s. ", source, target));
		}
		Path tar = Paths.get(target);
		Path parent = tar.getParent();
		if (parent != null && !Files.exists(parent)) {
			Files.createDirectories(parent);
		}
		Path src = Paths.get(source);
		Files.walkFileTree(src, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
				new SimpleFileVisitor<Path>() {
					@Override
					public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
						Path targetdir = tar.resolve(src.relativize(dir));
						try {
							Files.copy(dir, targetdir);
						} catch (FileAlreadyExistsException ex) {
							if (!Files.isDirectory(targetdir))
								throw ex;
						}
						return CONTINUE;
					}
	
					@Override
					public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
						Files.copy(file, tar.resolve(src.relativize(file)));
						return CONTINUE;
					}
				});
	}

	/**
	 * 删除指定的文件或目录，如果是目录，将会删除子目录及其包含的文件。
	 *
	 * @param filePath 待删除的文件或目录对象
	 * @throws IOException 删除过程中发生的异常
	 */
	public static void deleteFile(String filePath) throws IOException {
		Path path = Paths.get(filePath);
		if (path == null || !Files.exists(path)) {
			return;
		}
		Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
			@Override
			public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
				Files.delete(file);
				return CONTINUE;
			}

			@Override
			public FileVisitResult postVisitDirectory(Path dir, IOException ex) throws IOException {
				if (ex == null) {
					Files.delete(dir);
					return CONTINUE;
				} else {
					// directory iteration failed
					if (log.isErrorEnabled()) {
						log.error(String.format("Visit directory fail, path: %s.", dir.getFileName().toString()), ex);
					}
					throw ex;
				}
			}
		});
	}

	/**
	 * 将源文件移动到指定的目标文件
	 * @param src 源文件
	 * @param target 目标文件
	 * @throws IOException 移动过程中发生的异常
	 */
	public static void moveFile(String src, String target) throws IOException {
		Path path = Paths.get(target);
		Path parent = path.getParent();
		if (!Files.exists(parent)) {
			Files.createDirectories(parent);
		}
		Files.move(Paths.get(src), Paths.get(target), StandardCopyOption.REPLACE_EXISTING);
	}

	/**
	 * 将输入流中的数据保存到指定目录中的文件中
	 *
	 * @param filePath 目录
	 * @param fileName 文件名
	 * @param is       输入流
	 * @return 保存的文件路径
	 * @throws IOException 保存过程中发生的异常
	 */
	public static String saveFile(String filePath, String fileName, InputStream is) throws IOException {
		Path path = Paths.get(filePath, fileName);
		Path parent = path.getParent();
		if (!Files.exists(parent)) {
			Files.createDirectories(parent);
		}
		Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);
		return path.toFile().getAbsolutePath();
	}

	/**
	 * 将字节数组的内容保存到指定的文件
	 * 
	 * @param filePath	目录
	 * @param fileName	文件名
	 * @param content	字节数组
	 * @param isAppend	设置为true，内容将附加到文件末尾，否则将覆盖文件内容
	 * @return	保存文件的路径
	 * @throws IOException	保存过程中发生的异常
	 */
	public static String saveFile(String filePath, String fileName, byte[] content, boolean isAppend)
			throws IOException {
		Path path = Paths.get(filePath, fileName);
		Path parent = path.getParent();
		if (!Files.exists(parent)) {
			Files.createDirectories(parent);
		}
		OpenOption[] opAppend = { StandardOpenOption.WRITE, StandardOpenOption.APPEND };
		OpenOption[] opReplace = { StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING };
		Files.write(path, content, isAppend ? opAppend : opReplace);
		return path.toFile().getAbsolutePath();
	}

	/**
	 * 将按行分隔的内容保存到指定的文件
	 * 
	 * @param filePath	目录
	 * @param fileName	文件名
	 * @param content	按行分隔的列表
	 * @param isAppend	设置为true，内容将附加到文件末尾，否则将覆盖文件内容
	 * @return	保存文件的路径
	 * @throws IOException	保存过程中发生的异常
	 */
	public static String saveFile(String filePath, String fileName, List<String> content, boolean isAppend)
			throws IOException {
		Path path = Paths.get(filePath, fileName);
		Path parent = path.getParent();
		if (!Files.exists(parent)) {
			Files.createDirectories(parent);
		}
		OpenOption[] opAppend = { StandardOpenOption.WRITE, StandardOpenOption.APPEND };
		OpenOption[] opReplace = { StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING };
		Files.write(path, content, StandardCharsets.UTF_8, isAppend ? opAppend : opReplace);
		return path.toFile().getAbsolutePath();
	}

	/**
	 * 将指定的文件或目录压缩进ZIP文件
	 * 
	 * @param srcPath	待压缩的文件或目录路径
	 * @param zipPath	ZIP文件路径
	 */
	public static void zipFile(String srcPath, String zipPath) {
		File srcFile = new File(srcPath), zipFile = new File(zipPath);
		if (!srcFile.exists()) {
			if (log.isErrorEnabled()) {
				log.error(String.format("The file or directory[%s] no found.", srcPath));
			}
			return;
		}
		if (zipFile.exists()) {
			if (log.isWarnEnabled()) {
				log.warn(String.format("The zip file exist, it's content will be covered.", zipPath));
			}
		}
		zipFile.getParentFile().mkdirs();
		try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile), StandardCharsets.UTF_8)) {
			String rootPath = srcFile.getParent();
			byte[] buffer = new byte[1024 * 1024];
			zipFile(zip, rootPath, srcFile, in -> {
				int len = 0;
				try {
					while ((len = in.read(buffer)) > 0) {
						zip.write(buffer, 0, len);
					}
				} catch (IOException ex) {
					if (log.isErrorEnabled()) {
						log.error("Read file's content fail.", ex);
					}
				}
			});
		} catch (IOException ex) {
			if (log.isErrorEnabled()) {
				log.error(String.format("Zip the %s into %s fail.", srcPath, zipPath), ex);
			}
		}
	}

	/**
	 * 递归压缩ZIP
	 * 
	 * @param zip		ZipOutputStream
	 * @param rootPath	根路径，用于后续计算子路径
	 * @param srcFile	待压缩文件或目录路径
	 * @param consumer	文件读取消费
	 * @throws IOException 压缩过程中发生的异常
	 */
	private static void zipFile(ZipOutputStream zip, String rootPath, File srcFile, Consumer<InputStream> consumer)
			throws IOException {
		String entryName = srcFile.getAbsolutePath().substring(rootPath.length()).replace('\\', '/');
		if (srcFile.isFile()) {
			ZipEntry entry = new ZipEntry(entryName);
			zip.putNextEntry(entry);
			try (FileInputStream fin = new FileInputStream(srcFile)) {
				consumer.accept(fin);
			} catch (IOException ex) {
				throw ex;
			}
			zip.closeEntry();
		} else if (srcFile.isDirectory()) {
			File[] children = srcFile.listFiles();
			if (children.length == 0) {
				ZipEntry entry = new ZipEntry(entryName + "/");
				zip.putNextEntry(entry);
				zip.closeEntry();
			}
			for (File child : children) {
				zipFile(zip, rootPath, child, consumer);
			}
		}
	}

	/**
	 * 解压缩ZIP文件到指定目录
	 * 
	 * @param zipPath ZIP文件路径
	 * @param tarPath 目标文件夹路径
	 */
	public static void unzipFile(String zipPath, String tarPath) {
		File tarFile = new File(tarPath), zipFile = new File(zipPath);
		if (!zipFile.exists()) {
			if (log.isErrorEnabled()) {
				log.error(String.format("The zip file not found.", zipPath));
			}
			return;
		}
		if (tarFile.exists()) {
			if (log.isWarnEnabled()) {
				log.warn(String.format("The target directory[%s] exist, maybe any file be replaced.", tarPath));
			}
		} else {
			if (log.isWarnEnabled()) {
				log.warn(String.format("The target directory[%s] not found, will be create it.", tarPath));
			}
			tarFile.mkdirs();
		}
		byte[] buffer = new byte[1024 * 1024];
		try (ZipInputStream zip = new ZipInputStream(new FileInputStream(zipFile), StandardCharsets.UTF_8)) {
			ZipEntry entry;
			while ((entry = zip.getNextEntry()) != null) {
				File file = new File(tarPath, entry.getName());
				if (entry.isDirectory()) {
					file.mkdirs();
				} else {
					file.getParentFile().mkdirs();
					try (FileOutputStream fout = new FileOutputStream(file)) {
						int len;
						while ((len = zip.read(buffer)) > 0) {
							fout.write(buffer, 0, len);
						}
					} catch (IOException ex) {
						throw ex;
					} finally {
						zip.closeEntry();
					}
				}
				zip.closeEntry();
			}
		} catch (IOException ex) {
			if (log.isErrorEnabled()) {
				log.error(String.format("Unzip the %s into %s fail.", zipPath, tarPath), ex);
			}
		}
	}
}
